/* Editor.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Editor class.
*/

#include "Editor.h"

#include "_paste/Pasteboard.h"

#include <sstream>

using std::istringstream;



// Default constructor:
Editor::Editor()
	: tasks(categories.category), editMode(EDIT_NOTHING), PAGE_ROWS(30)
{
}



// Initialize:
// Load the data file from the given directory.
bool Editor::Initialize(const string &dataFilePath)
{
	editMode = EDIT_NOTHING;

	return categories.Load(dataFilePath);
}



// Initialize:
// Load the data file from the given directory.
bool Editor::InitializeFromData(const string &data)
{
	istringstream in(data);
	return categories.Load(in);
}



// Get the path to the file we're editing.
const string &Editor::FilePath() const
{
	return categories.FilePath();
}



// Save:
// Save the data file.
void Editor::Save()
{
	categories.Save();
}



// Revert:
// Revert to the saved version of the data.
void Editor::Revert()
{
	// Stop editing first, so we don't end up carrying over the current edit.
	ChangeEditMode(EDIT_NOTHING);

	categories.Revert();
}



// Resurrect:
// Load all categories that have been deleted and append them
// to the currently open set of categories.
void Editor::Resurrect()
{
	categories.Resurrect();
}



// SelectCategory:
// Select the category with the given index, if it exists.
// This is for handling a mouse click, so if the current category is selected,
// edit it.
void Editor::SelectCategory(int category)
{
	if((category >= categories.Count()) | (category < 0))
		return;
	
	if(category == categories.Selection())
		ChangeEditMode(EDIT_CATEGORY);
	else
	{
		ChangeEditMode(EDIT_NOTHING);
		categories.Select(category);
	}
}



// ToggleExpansion:
// This is primarily a mouse click handler. Toggles expansion of the given row.
void Editor::ToggleExpansion(int row, bool recursive)
{
	// Changes to the tree break us out of editing mode.
	ChangeEditMode(EDIT_NOTHING);
	tasks->SelectRow(row);
	
	bool isExpanded = (*tasks)[row].IsExpanded();
	if(isExpanded)
	{
		if(recursive)
			tasks->RecursiveCollapse();
		else
			tasks->Collapse();
	}
	else
	{
		if(recursive)
			tasks->RecursiveExpand();
		else
			tasks->Expand();
	}
}



// ToggleCheck:
// This is primarily a mouse click handler. Toggles checking of the given row.
void Editor::ToggleCheck(int row, bool recursive)
{
	// Changes to the tree break us out of editing mode.
	ChangeEditMode(EDIT_NOTHING);
	tasks->SelectRow(row);
	
	if(recursive)
		tasks->RecursiveToggle(Task::CHECKED);
	else
		tasks->Toggle(Task::CHECKED);
}



// SelectText:
// This is primarily a mouse click handler. Select the given row. If it is the
// currently selected row, edit its text.
void Editor::SelectText(int row)
{
	ChangeEditMode(EDIT_NOTHING);
	if(tasks->GetSelection().Contains(row))
		ChangeEditMode(EDIT_TEXT, row);
	else
		tasks->SelectRow(row);
}



// SelectDate:
// This is primarily a mouse click handler. Select the given row. If it is the
// currently selected row, edit its date.
void Editor::SelectDate(int row)
{
	ChangeEditMode(EDIT_NOTHING);
	if(tasks->GetSelection().Contains(row))
		ChangeEditMode(EDIT_DATE, row);
	else
		tasks->SelectRow(row);
}



// ExpandSelection:
// This is primarily a mouse click handler. Move the selection cursor to the
// given row, without modifying the selection anchor.
void Editor::ExpandSelection(int row)
{
	tasks->SetCursor(row);
}



// HandleKey:
// Handle the given key event.
bool Editor::HandleKey(SDLKey key, SDLMod mod)
{
	// If we're currently editing text, handle text input.
	if(HandleText(key, mod))
		return true;
	
	// Check whether the root node (the category name) is selected.
	bool isRoot = (tasks->GetSelection() == -1);

	// Get the state of the modifier keys.
	bool controlKey = ((mod & (KMOD_META | KMOD_CTRL)) != 0);
	bool shiftKey = ((mod & KMOD_SHIFT) != 0);
	bool altKey = ((mod & KMOD_ALT) != 0);
	
	// Handle key commands. If the command is not recognized, this just
	// passes through to the main switch.
	if(controlKey)
	{
		switch(key)
		{
			case 't':
				// Sort by text.
				tasks->SortAlphabetical(shiftKey);
				return true;

			case 'd':
				// Sort by date.
				tasks->SortByDate(shiftKey);
				return true;

			case 'a':
				// Select all.
				tasks->SelectAll();
				return true;

			case 'x':
				// Cut.
				Pasteboard::Set(tasks->Copy(!shiftKey));
				tasks->DeleteSelection();
				return true;

			case 'c':
				// Copy.
				Pasteboard::Set(tasks->Copy(!shiftKey));
				return true;

			case 'v':
				// Paste.
				tasks->Paste(Pasteboard::Get(), shiftKey);
				return true;
			
			default:
				break;
		}
	}
	
	switch(key)
	{
		case '/':
			// Begin searching, either for a line beginning with the entered text,
			// or for a line containing the entered text.
			BeginSearch(shiftKey);
			break;
			
		case SDLK_HOME:
			// Select the root of the tree.
			tasks->SelectRoot();
			break;
			
		case SDLK_END:
			// Select the last line.
			tasks->SelectEnd();
			break;
			
		case SDLK_PAGEUP:
			// Move selection up one page.
			tasks->SelectChange(-PAGE_ROWS);
			break;
			
		case SDLK_PAGEDOWN:
			// Move selection down one page.
			tasks->SelectChange(PAGE_ROWS);
			break;

		case SDLK_UP:
			// Depending on the modifiers, either select the previous task, expand the selection upward,
			// move the selected task up, move this category up, or select the previous category.
			if(altKey & controlKey)
				categories.MoveUp();
			else if(altKey)
				categories.SwitchToPrevious();
			else if(controlKey)
				tasks->MoveUp();
			else if(shiftKey)
				tasks->CursorUp();
			else
				tasks->SelectUp();
			break;

		case SDLK_DOWN:
			// Depending on the modifiers, either select the next task, expand the selection downward,
			// move the selected task down, move this category down, or select the next category.
			if(altKey & controlKey)
				categories.MoveDown();
			else if(altKey)
				categories.SwitchToNext();
			else if(controlKey)
				tasks->MoveDown();
			else if(shiftKey)
				tasks->CursorDown();
			else
				tasks->SelectDown();
			break;

		case SDLK_LEFT:
			// Depending on the modifers, either outdent the currently selected task, or collapse it.
			if(controlKey)
				tasks->MoveOut();
			else if(shiftKey)
				tasks->RecursiveCollapse();
			else
				tasks->Collapse();
			break;
	
		case SDLK_RIGHT:
			// Depending on the modifers, either indent the currently selected task, or expand it.
			if(controlKey)
				tasks->MoveIn();
			else if(shiftKey)
				tasks->RecursiveExpand();
			else
				tasks->Expand();
			break;
			
		case '[':
			// Depending on the modifiers, select the parent of this task or the previous sibling.
			if(shiftKey)
				tasks->SelectParent();
			else
				tasks->SelectSiblingUp();
			break;
			
		case ']':
			// Depending on the modifiers, select the next task or the next sibling.
			if(shiftKey)
				tasks->SelectDown();
			else
				tasks->SelectSiblingDown();
			break;

		case '.':
			// Toggle the check on the selected lines. If the shift key is down, do it recursively.
			if(shiftKey)
				tasks->RecursiveToggle(Task::CHECKED);
			else
				tasks->Toggle(Task::CHECKED);
			break;

		case '=':
			// Toggle the emphasis of the selected lines. If the shift key is down, do it recursively.
			if(shiftKey)
				tasks->RecursiveToggle(Task::EMPHASIZED);
			else
				tasks->Toggle(Task::EMPHASIZED);
			break;

		case SDLK_DELETE:
		case SDLK_BACKSPACE:
			// Depending on the modifiers, either delete the current category, delete the selected tasks,
			// or toggle crossing out of the selected tasks.
			if(altKey)
			{
				ChangeEditMode(EDIT_NOTHING);
				categories.Delete();
			}
			else if(controlKey)
				tasks->DeleteSelection();
			else if(shiftKey)
				tasks->RecursiveToggle(Task::DELETED);
			else
				tasks->Toggle(Task::DELETED);
			break;
	
		case SDLK_TAB:
			// Edit the selected task. If the root is selected edit this category's name.
			if(altKey | isRoot)
				ChangeEditMode(EDIT_CATEGORY);
			else if(shiftKey)
				ChangeEditMode(EDIT_DATE, tasks->GetSelection().First());
			else
				ChangeEditMode(EDIT_TEXT, tasks->GetSelection().First());
			break;

		case SDLK_RETURN:
			// Depending on the modifiers, add either a new category, a new child task, or a 
			// new sibling task. Edit the text of that new task or category.
			if(altKey)
			{
				categories.Add();
				ChangeEditMode(EDIT_CATEGORY);
			}
			else
			{
				if(shiftKey)
					tasks->InsertChild();
				else
					tasks->InsertSibling();

				ChangeEditMode(EDIT_TEXT, tasks->GetSelection().First());
			}
			break;

		default:
			// The event was not recognized, so nothing was done.
			return false;
	}

	return true;
}



// For editing the preferences file: create or select this category.
TaskList &Editor::GetCategory(const string &name)
{
	CategoryList::iterator it = categories.begin();
	for( ; it != categories.end(); ++it)
		if(it->Name() == name)
			break;
	if(it == categories.end())
	{
		// TODO: this is rather ugly.
		categories.SwitchToFirst();
		categories.SwitchToPrevious();
		categories.Add();
		(*--it)[0].SetText(name);
	}
	
	return *it;
}



// After hand-modifying that category, it must be Update()d.
void Editor::Update(const string &name, list<Task> &newList)
{
	CategoryList::iterator it = categories.begin();
	for( ; it != categories.end(); ++it)
		if(it->Name() == name)
			it->SwapList(newList);
}



// BeginSearch:
// Enter search mode. Begin searching at the currently selected line.
void Editor::BeginSearch(bool anywhereInLine)
{
	searchAnywhere = anywhereInLine;
	searchBeginRow = tasks->GetSelection();
	if(searchBeginRow == -1)
		searchBeginRow = 0;
	ChangeEditMode(EDIT_SEARCH);
}



// HandleText:
// Do text editing based on the given keypress. Return false if the key event
// should be handled by the main handler as well.
bool Editor::HandleText(SDLKey key, SDLMod mod)
{
	// Do nothing if we are not editing text right now.
	if(editMode == EDIT_NOTHING)
		return false;
	
	// Do nothing if tis key press is a modifier key.
	if((key == SDLK_LSHIFT) | (key == SDLK_RSHIFT) | (key == SDLK_LCTRL) | (key == SDLK_RCTRL) |
			(key == SDLK_LALT) | (key == SDLK_RALT) | (key == SDLK_LMETA) | (key == SDLK_RMETA))
		return true;
	
	// Get the state of the modifier keys.
	bool controlKey = ((mod & (KMOD_META | KMOD_CTRL)) != 0);
	bool shiftKey = ((mod & KMOD_SHIFT) != 0);

	// Handle copy / cut / paste / select all.
	if(controlKey)
	{
		if(key == 'a')
			textSelection.SetFirstAndLast(0, static_cast<int>(editString.length()));
		else if((key == 'c') | (key == 'x'))
		{
			Pasteboard::Set(editString.substr(textSelection.First(), textSelection.Count() - 1));
			if(key == 'x')
			{
				editString.erase(textSelection.First(), textSelection.Count() - 1);
				textSelection = textSelection.First();
			}
		}
		else if(key == 'v')
		{
			string text = Pasteboard::Get();
			editString.replace(textSelection.First(), textSelection.Count() - 1, text);
			textSelection = textSelection.First() + static_cast<int>(text.length());
		}
		// These are the only control key combinations handled here.
		else
		{
			ChangeEditMode(EDIT_NOTHING);
			return false;
		}

		// If the key combination was handled we are done.
		return true;
	}

	// Switch based on the key that was pressed.
	if(key == SDLK_LEFT)
	{
		// Move or expand the selection to the left.
		if(shiftKey)
		{
			textSelection--;
			if(textSelection < 0)
				textSelection++;
		}
		else if(textSelection.First() == 0)
				textSelection = textSelection.First();
		else if(textSelection.IsSingle())
			--textSelection;
		else
			textSelection = textSelection.First();
	}
	else if(key == SDLK_RIGHT)
	{
		// Move or expand the selection to the right.
		if(shiftKey)
		{
			textSelection++;
			if(textSelection > static_cast<int>(editString.length()))
				textSelection--;
		}
		else if(textSelection.Last() == static_cast<int>(editString.length()))
			textSelection = textSelection.Last();
		else if(textSelection.IsSingle())
			++textSelection;
		else
			textSelection = textSelection.Last();
	}
	else if(key == SDLK_BACKSPACE)
	{
		// Delete the currently selected text.
		// If the selection is empty, delete the character before the cursor.
		// Delete does nothing if the cursor is at the beginning of the string.
		if(textSelection.Last())
		{
			if(editMode == EDIT_SEARCH)
			{
				// Special case: when editing the search string, just delete one character even though
				// the whole string is "selected".
				editString.erase(editString.length() - 1);
				textSelection.SetFirstAndLast(0, textSelection.Last() - 1);
			}
			else
			{
				int newSelection = textSelection.First() - textSelection.IsSingle();
				editString.erase(newSelection, textSelection.Count() - !textSelection.IsSingle());
				textSelection = newSelection;
			}
		}
	}
	else if((key >= ' ') & (key <= '~'))
	{
		// Handle ordinary character input.
		// If the shift key is down, map the key to whatever shift turns that key into.
		char value = shiftKey ? SHIFT[key - ' '] : key;
		
		if(editMode == EDIT_SEARCH)
			editString.push_back(value);
		else
		{
			// Replace the selected text, if any, with the newly typed character.
			if(!textSelection.IsSingle())
				editString.erase(textSelection.First(), textSelection.Count() - 1);

			editString.insert(textSelection.First(), 1, value);

			textSelection = textSelection.First() + 1;
		}
	}
	else if(key == SDLK_TAB)
	{
		// If searching, the tab key moves to the next search hit. Otherwise, the tab key selects
		// the next field in the selected lines.
		if(editMode == EDIT_SEARCH)
			searchBeginRow = tasks->GetSelection() + 1;
		else
		{
			int newRow = editRow;
			if(editMode == EDIT_TEXT)
			{
				if(shiftKey)
					newRow = (editRow == tasks->GetSelection().First()) ? tasks->GetSelection().Last() : editRow - 1;

				ChangeEditMode(EDIT_DATE, newRow);
			}
			else if(editMode == EDIT_DATE)
			{
				if(!shiftKey)
					newRow = (editRow == tasks->GetSelection().Last()) ? tasks->GetSelection().First() : editRow + 1;

				ChangeEditMode(EDIT_TEXT, newRow);
			}
			else
				ChangeEditMode(EDIT_NOTHING);
			return true;
		}
	}
	else
	{
		// Any other key press exits editing mode. That key press must be handled by the main
		// even loop unless it is just a press of the return key.
		ChangeEditMode(EDIT_NOTHING);
		// If the user presses return, do no additional action.
		return (key == SDLK_RETURN);
	}
	
	if(editMode == EDIT_SEARCH && !editString.empty())
	{
		// Search for the entered text. If we reach the end of the list, begin again at the
		// start of the list.
		int row = Search(searchBeginRow, tasks->Rows());
		if(row == tasks->Rows())
			row = Search(0, searchBeginRow);
		
		// Select whichever row the seach returned. If no hit was found, the second search above
		// will have returned searchBeginRow, so the row selection will not change.
		tasks->SelectRow(row);
		editRow = row;
	}
	
	return true;
}



// ChangeEditMode:
// Change to the given editing mode. Save whatever string we were previously editing, if any.
void Editor::ChangeEditMode(Mode newMode, int newRow)
{
	// Save the current string.
	if(editMode == EDIT_TEXT)
		(*tasks)[editRow].SetText(editString);
	else if(editMode == EDIT_DATE)
		(*tasks)[editRow].SetDate(editString);
	else if(editMode == EDIT_CATEGORY)
		tasks->SetName(editString);

	editMode = newMode;
	editRow = newRow;
	
	// Load the edit string from the proper field.
	if(editMode == EDIT_TEXT)
		editString = (*tasks)[editRow].Text();
	else if(editMode == EDIT_DATE)
		editString = (*tasks)[editRow].Date();
	else if(editMode == EDIT_CATEGORY)
		editString = tasks->Name();
	else
		editString.clear();

	// Select the entire string.
	textSelection.SetFirstAndLast(0, static_cast<int>(editString.length()));
}



// Search:
// Search for text. Return the first matching row.
int Editor::Search(int firstRow, int endRow)
{
	for(int row = firstRow; row < endRow; ++row)
		if((*tasks)[row].SearchText(editString, textSelection, !searchAnywhere))
			return row;
	
	return endRow;
}



// This array maps keys to the values they have when the Shift key is pressed.
const char Editor::SHIFT[95] = {
	' ', '1', '\'', '3', '4', '5', '7', '"', '9', '0', '8', '=', '<', '_', '>', '?',
	')', '!', '@', '#', '$', '%', '^', '&', '*', '(', ';', ':', ',', '+', '.', '/',
	'2', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
	'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '6', '-',
	'~', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
	'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '`'
};



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
